// ==UserScript== // @name Bilibili 按标签、标题、时长、UP主屏蔽视频 // @namespace https://github.com/tjxwork // @version 1.1.5 // @note // @note 新版本的视频介绍,来拯救一下我可怜的播放量吧 ●︿● // @note 应该是目前B站最强的屏蔽视频插件?【tjxgame】 // @note https://www.bilibili.com/video/BV1WJ4m1u79n // @note // @note 作者的爱发电:https://afdian.com/a/tjxgame // @note 欢迎订阅支持、提需求,您的赞助支持就是维护更新的最大动力! // @note // @note v1.1.5 修正导致缓存记录对象的 videoLink 记录出错的部分代码; 修改赞助按钮的跳出连接; (我真的是不知道什么鬼运气,去哪哪崩,刚开的爱发电也崩了。) // @note v1.1.4 添加新功能:“屏蔽叠加层的提示只显示类型”,有部分用户可能连命中的屏蔽词都不想看到,但是又倾向使用叠加层模式,所以增加了这个开关。 // @note 感谢来自爱发电的赞助需求。 // @note v1.1.3 兼容脚本处理:[bv2av](https://greasyfork.org/zh-CN/scripts/398535)(此脚本会将视频链接替换为旧的 AV 号链接),感谢 @Henry-ZHR 的提出; // @note 不完善功能修复:每次触发运行时,会将屏蔽叠加背景层与父元素尺寸进行同步,解决了页面布局变化时叠加层不跟随变化,感谢 @Henry-ZHR 的建议; // @note “隐藏首页等页面的非视频元素” 功能生效范围增加:隐藏 搜索页——综合 下的 直播卡片 // @note v1.1.2 添加新功能:“按置顶评论屏蔽”; // @note 注意:“按置顶评论屏蔽”、“屏蔽精选评论的视频” 这两个功能都用到了获取评论的API, // @note 这个API对请求频率非常敏感,频繁刷新或者开启新页面会导致B站拒绝请求,正常浏览一般不会出现拒绝问题。 // @note v1.1.1 添加新功能:“屏蔽充电专属的视频”; // @note v1.1.0 添加新功能:“屏蔽精选评论的视频”,骗子视频大概率会开启精选评论; // @note “隐藏首页等页面的非视频元素” 功能生效范围增加:隐藏视频播放页右侧视频相关的游戏推荐; // @note 控制台输出日志优化:现在只有发生变化的时候才会输出; // @note v1.0.2 “隐藏首页等页面的非视频元素” 功能生效范围增加:隐藏视频播放页右侧最下方的“大家围观的直播” // @note v1.0.1 修正了B站旧版首页的顶部推荐条失效的Bug; // @note 如果用旧版首页只是想要更多的顶部推荐的话,建议使用 bilibili-app-recommend 来获取更多的推荐。 // @note 现在版本B站首页的推荐卡片有广告的问题,可以通过本脚本的 “隐藏首页等页面的非视频元素” 功能来解决。 // @note v1.0.0 菜单UI使用Vue3重构,现在不用担心缩放问题挡住UI了,界面更加现代化; // @note 改进了判断逻辑,现在可以使用白名单来避免误杀关注的UP了; // @note 新增功能:视频分区屏蔽、播放量屏蔽、点赞率屏蔽、竖屏视频屏蔽、UP主名称正则屏蔽、隐藏非视频元素、白名单避免屏蔽指定UP。 // @description 对Bilibili的视频卡片,以标签、标题、UP主、时长、竖屏、充电、评论等信息来屏蔽视频,附带去除视频卡片中的直播、广告、推广内容的功能。 // @author tjxwork // @license CC-BY-NC-SA // @icon https://www.bilibili.com/favicon.ico // @match https://www.bilibili.com/* // @match https://www.bilibili.com/v/popular/all/* // @match https://www.bilibili.com/v/popular/weekly/* // @match https://www.bilibili.com/v/popular/history/* // @exclude https://www.bilibili.com/anime/* // @exclude https://www.bilibili.com/movie/* // @exclude https://www.bilibili.com/guochuang/* // @exclude https://www.bilibili.com/variety/* // @exclude https://www.bilibili.com/tv/* // @exclude https://www.bilibili.com/documentary* // @exclude https://www.bilibili.com/mooc/* // @exclude https://www.bilibili.com/v/virtual/* // @exclude https://www.bilibili.com/v/popular/music/* // @exclude https://www.bilibili.com/v/popular/drama/* // @match https://search.bilibili.com/* // @exclude https://search.bilibili.com/live // @grant GM_registerMenuCommand // @grant GM_setValue // @grant GM_getValue // @grant GM_addStyle // @require https://lf3-cdn-tos.bytecdntp.com/cdn/expire-1-w/vue/3.2.31/vue.global.min.js // ==/UserScript== "use strict"; // --------------------参数变量初始化-------------------- // 初始化屏蔽参数变量,从 油猴扩展存储 读取到 blockedParameter let blockedParameter = GM_getValue("GM_blockedParameter", { // 屏蔽标题 blockedTitle_Switch: true, blockedTitle_UseRegular: true, blockedTitle_Array: [], // 屏蔽Up主和Uid blockedNameOrUid_Switch: true, blockedNameOrUid_UseRegular: false, blockedNameOrUid_Array: [], // 屏蔽视频分区 blockedVideoPartitions_Switch: true, blockedVideoPartitions_UseRegular: false, blockedVideoPartitions_Array: [], // 屏蔽标签 blockedTag_Switch: true, blockedTag_UseRegular: true, blockedTag_Array: [], // 屏蔽双重屏蔽标签 doubleBlockedTag_Switch: true, doubleBlockedTag_UseRegular: true, doubleBlockedTag_Array: [], // 屏蔽短时长视频 blockedShortDuration_Switch: false, blockedShortDuration: 0, // 屏蔽低播放量视频 blockedBelowVideoViews_Switch: false, blockedBelowVideoViews: 0, // 屏蔽低于指定点赞率的视频 blockedBelowLikesRate_Switch: false, blockedBelowLikesRate: 0, // 屏蔽竖屏视频 blockedPortraitVideo_Switch: false, // 屏蔽充电专属的视频 blockedChargingExclusive_Switch: false, // 屏蔽精选评论的视频 blockedFilteredCommentsVideo_Switch: false, // 屏蔽置顶评论 blockedTopComment_Switch: false, blockedTopComment_UseRegular: true, blockedTopComment_Array: [], // 白名单Up主和Uid whitelistNameOrUid_Switch: false, whitelistNameOrUid_Array: [], // 隐藏非视频元素 hideNonVideoElements_Switch: true, // 屏蔽叠加层的提示只显示类型而不显示命中项 blockedOverlayOnlyDisplaysType_Switch: false, // 隐藏视频而非叠加层模式 hideVideoMode_Switch: false, // 控制台输出日志 consoleOutputLog_Switch: false, }); // 旧参数适配 function oldParameterAdaptation(obj) { //判断是否为旧参数,是的话就修改为新参数结构 if (Object.prototype.hasOwnProperty.call(obj, "blockedTitleArray")) { // 屏蔽标题 obj["blockedTitle_Switch"] = true; obj["blockedTitle_UseRegular"] = true; obj["blockedTitle_Array"] = obj["blockedTitleArray"]; delete obj["blockedTitleArray"]; // 屏蔽Up主和Uid obj["blockedNameOrUid_Switch"] = true; obj["blockedNameOrUid_UseRegular"] = true; obj["blockedNameOrUid_Array"] = obj["blockedNameOrUidArray"]; delete obj["blockedNameOrUidArray"]; // 屏蔽视频分区 obj["blockedVideoPartitions_Switch"] = false; obj["blockedVideoPartitions_UseRegular"] = false; obj["blockedVideoPartitions_Array"] = []; // 屏蔽标签 obj["blockedTag_Switch"] = true; obj["blockedTag_UseRegular"] = true; obj["blockedTag_Array"] = obj["blockedTagArray"]; delete obj["blockedTagArray"]; // 屏蔽双重屏蔽标签 obj["doubleBlockedTag_Switch"] = true; obj["doubleBlockedTag_UseRegular"] = true; obj["doubleBlockedTag_Array"] = obj["doubleBlockedTagArray"]; delete obj["doubleBlockedTagArray"]; // 屏蔽短时长视频 obj["blockedShortDuration_Switch"] = true; // 白名单Up主和Uid obj["whitelistNameOrUid_Switch"] = false; obj["whitelistNameOrUid_Array"] = []; // 隐藏视频而非叠加层模式 obj["hideVideoMode_Switch"] = obj["hideVideoModeSwitch"]; delete obj["hideVideoModeSwitch"]; // 控制台输出日志 obj["consoleOutputLog_Switch"] = obj["consoleOutputLogSwitch"]; delete obj["consoleOutputLogSwitch"]; } } oldParameterAdaptation(blockedParameter); // --------------------菜单UI部分-------------------- // 菜单UI的CSS,使用 GM_addStyle 注入 CSS GM_addStyle(` :root { /* 主窗体背景色 */ --uiBackgroundColor: rgb(48, 48, 48); /* 输入模块背景色 */ --uiInputContainerBackgroundColor: rgb(64, 64, 64); /* 输入框背景色 */ --uiInputBoxBackgroundColor: rgb(89, 89, 89); /* 滚动条背景色 */ --uiScrollbarBackgroundColor: rgb(141, 141, 141); /* 文字颜色 */ --uiTextColor: rgb(250, 250, 250); /* 按钮色 */ --uiButtonColor: rgb(0, 174, 236); /* 边框色 */ --uiBorderColor: rgba(0, 0, 0, 0); /* 提醒框背景色 */ --uiPromptBoxColor: rgb(42, 44, 53); /* 屏蔽叠加层背景色 */ --blockedOverlayColor: rgba(60, 60, 60, 0.85); /* 字体大小 */ --fontSize: 14px; /* 行高 */ --lineHeight: 24px; /* 圆角 */ --borderRadius: 4px; } /* 菜单UI */ #blockedMenuUi { font-size: var(--fontSize); position: fixed; bottom: 4vh; right: 2vw; z-index: 1005; width: 460px; max-height: 90vh; overflow-y: auto; background-color: var(--uiBackgroundColor); } #blockedMenuUi, #blockedMenuUi * { color: var(--uiTextColor); box-sizing: border-box; border-style: solid; border-width: 0px; border-color: var(--uiBorderColor); border-radius: var(--borderRadius); line-height: var(--lineHeight); vertical-align: middle; font-family: "Cascadia Mono", Monaco, Consolas, "PingFang SC", "Helvetica Neue", "Microsoft YaHei", sans-serif; } /* 滚动条 */ #blockedMenuUi::-webkit-scrollbar, #blockedMenuUi ul::-webkit-scrollbar { width: 7px; } /* 滚动条 轨道*/ #blockedMenuUi::-webkit-scrollbar-track, #blockedMenuUi ul::-webkit-scrollbar-track { background: var(--uiScrollbarBackgroundColor); border-radius: 7px; } /* 滚动条 滑块*/ #blockedMenuUi::-webkit-scrollbar-thumb, #blockedMenuUi ul::-webkit-scrollbar-thumb { background: var(--uiInputContainerBackgroundColor); border-radius: 7px; } /* 滚动条 滑块 鼠标经过 */ #blockedMenuUi::-webkit-scrollbar-thumb:hover, #blockedMenuUi ul::-webkit-scrollbar-thumb:hover { background: var(--uiInputBoxBackgroundColor); border-radius: 7px; } /* 滚动条 滑块 鼠标点击 */ #blockedMenuUi::-webkit-scrollbar-thumb:active, #blockedMenuUi ul::-webkit-scrollbar-thumb:active { background: var(--uiButtonColor); border-radius: 7px; } #menuTitle { font-size: 17px; text-align: center; margin: 10px; } .menuOptions { background-color: var(--uiInputContainerBackgroundColor); padding: 10px; margin: 0 10px; margin-bottom: 10px; } .titleLabelLeft { display: inline-block; width: 275px; margin-bottom: 5px; } .titleLabelRight { display: inline-block; margin-bottom: 5px; } #blockedMenuUi label { font-size: 16px; vertical-align: middle; } #blockedMenuUi input { background-color: var(--uiInputBoxBackgroundColor); font-size: var(--fontSize); line-height: var(--lineHeight); border-radius: var(--borderRadius); padding: 0 5px; margin-bottom: 5px; width: 360px; vertical-align: middle; } #blockedMenuUi input[type="number"] { width: 4em; margin: 0 5px; padding: 0 5px; text-align: right; appearance: none; } #blockedMenuUi input[type="number"]::-webkit-inner-spin-button, #blockedMenuUi input[type="number"]::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; } #blockedMenuUi input[type="checkbox"] { width: 16px; height: 16px; margin: 0; margin-bottom: 2.5px; margin-right: 5px; appearance: none; border: 1.5px solid var(--uiTextColor); border-radius: 8px; } #blockedMenuUi input[type="checkbox"]:checked { border: 3px solid; background-color: var(--uiButtonColor); } #blockedMenuUi button { line-height: var(--lineHeight); border-radius: var(--borderRadius); padding: 0; margin-bottom: 5px; margin-left: 5px; width: 45px; vertical-align: middle; background-color: var(--uiButtonColor); transition: background-color 0.1s ease; } #blockedMenuUi button:hover { background-color: rgb(17, 154, 204); } #blockedMenuUi button:active { background-color: rgb(62, 203, 255); } #blockedMenuUi ul { background-color: var(--uiInputBoxBackgroundColor); font-size: 14px; padding: 5px 5px 0px 0px; margin-inline: 0px; margin: 0; width: 100%; min-height: 34px; max-height: 92px; overflow-y: auto; } #blockedMenuUi li { line-height: var(--lineHeight); border-radius: var(--borderRadius); display: inline-block; padding: 0 5px; margin-bottom: 5px; margin-left: 5px; vertical-align: middle; background-color: var(--uiButtonColor); } #blockedMenuUi li button { width: 20px; margin: 0px; padding: 0 0 3px 0; font-size: 24px; line-height: 18px; border: 0px; } #blockedMenuUi li button:hover { background-color: var(--uiButtonColor); color: rgb(221, 221, 221); } #blockedMenuUi li button:active { background-color: var(--uiButtonColor); color: var(--uiButtonColor); } #blockedMenuUi textarea { background-color: var(--uiInputBoxBackgroundColor); font-size: 14px; padding: 0 5px; width: 100%; resize: none; } #menuButtonContainer { position: sticky; right: 0; bottom: 0; width: 100%; background-color: var(--uiBackgroundColor); margin-top: -10px; } #menuButtonContainer button { line-height: var(--lineHeight); border-radius: var(--borderRadius); font-size: 16px; border: 0; padding: 0; margin-top: 10px; margin-bottom: 10px; margin-left: 10px; height: 45px; width: 45px; vertical-align: middle; background-color: var(--uiButtonColor); } #menuButtonContainer label { line-height: 45px; border-radius: var(--borderRadius); display: inline-block; border: 0; padding: 0; margin: 10px 20px; height: 45px; width: 130px; vertical-align: middle; text-align: center; background-color: var(--uiInputBoxBackgroundColor); transition: opacity 1s; } /* 支付宝微信二维码 */ #alipayWeChatQrCode { position: fixed; top: 52%; left: 16%; transform: translate(0%, -50%); box-shadow: 0 8px 8px rgb(85 85 85 / 85%); } `); // 菜单UI的HTML let menuUiHTML = `
感谢赞助
`; // 菜单UI function blockedMenuUi() { // 检查页面中是否已经存在这个元素 if (!document.getElementById("blockedMenuUi")) { // 如果不存在,将菜单弹窗添加到页面 // 创建Div作为菜单容器 let menuUi = document.createElement("div"); menuUi.innerHTML = menuUiHTML; document.body.appendChild(menuUi); } else { console.log("菜单 #blockedMenuUi 已存在"); return; } // 让油猴脚本的Vue代码能网页中正常工作。 unsafeWindow.Vue = Vue; const { createApp, reactive, toRaw } = Vue; createApp({ setup() { // 设置选项数据 const menuUiSettings = reactive({}); // 临时存储的各数组对应的输入值 const tempInputValue = reactive({ blockedTitle_Array: "", blockedNameOrUid_Array: "", blockedVideoPartitions_Array: "", blockedTag_Array: "", doubleBlockedTag_Array: "", blockedTopComment_Array: "", whitelistNameOrUid_Array: "", // 临时提示文本 promptText_Switch: true, promptText_Opacity: 0, promptText: "", // 二维码显示开关 QrCode_Switch: false, }); function showPromptText(text) { // tempInputValue.promptText_Switch = true; // 显示 label 元素 tempInputValue.promptText_Opacity = 1; tempInputValue.promptText = text; // 1.5秒后隐藏 label 元素 setTimeout(() => { // tempInputValue.promptText_Switch = false; tempInputValue.promptText_Opacity = 0; }, 1500); } // 添加数组项目 const addArrayButton = (tempInputValue, menuUiSettings, keyName) => { // 确保 menuUiSettings[keyName] 是一个数组 if (!Array.isArray(menuUiSettings[keyName])) { menuUiSettings[keyName] = []; } // 双重标签的特殊处理 判断是否为空 if (keyName == "doubleBlockedTag_Array" && tempInputValue[keyName].trim()) { // 使用 split 按逗号分隔,然后映射去除每个标签的首尾空白 const items = tempInputValue[keyName] .split(",") .map((item) => item.split("|").map((str) => str.trim())) .filter((subArray) => subArray.length === 2 && subArray.every((str) => str !== "")); items.forEach((secondSplitItem) => { // 将两个标签重新组合成一个字符串,并添加到设置数据中 const formattedItem = secondSplitItem.join("|"); menuUiSettings[keyName].push(formattedItem); }); // 清空输入框内容 tempInputValue[keyName] = ""; return; } // 判断是否为空 if (tempInputValue[keyName].trim()) { // 用逗号分隔值并去除每项的空格后添加到数组 const items = tempInputValue[keyName].split(",").map((item) => item.trim()); menuUiSettings[keyName].push(...items); // 清空输入框内容 tempInputValue[keyName] = ""; } }; //删除数组项目 const delArrayButton = (index, array) => { //splice(要删除元素的索引位置, 要删除的元素数量) array.splice(index, 1); }; // 读取按钮 深拷贝函数,递归处理嵌套对象,普通对象 to 普通对象/响应式对象 function deepCopy(source, target) { for (let key in source) { if (typeof source[key] === "object" && source[key] !== null) { target[key] = Array.isArray(source[key]) ? [] : {}; // 根据类型创建空对象或数组 deepCopy(source[key], target[key]); // 递归拷贝子对象 } else { target[key] = source[key]; // 复制基本类型和函数等 } } } // 读取按钮 const refreshButton = () => { // 使用 deepCopy 函数进行深拷贝 deepCopy(blockedParameter, menuUiSettings); showPromptText("读取数据"); }; // 保存按钮 深拷贝函数,递归处理响应式对象,响应式对象 to 普通对象 function deepCopyReactiveObject(reactiveObj, targetObj) { for (let key in reactiveObj) { const rawValue = toRaw(reactiveObj[key]); // 获取属性的原始值 if (typeof rawValue === "object" && rawValue !== null) { targetObj[key] = Array.isArray(rawValue) ? [] : {}; // 根据类型创建空对象或数组 deepCopyReactiveObject(rawValue, targetObj[key]); // 递归处理嵌套的响应式子对象 } else { targetObj[key] = rawValue; // 复制基本类型和函数等 } } } // 保存按钮 const saveButton = () => { // 将响应式对象深拷贝到普通对象 blockedParameter deepCopyReactiveObject(menuUiSettings, blockedParameter); // 将全局屏蔽参数对象变量 blockedParameter 保存到油猴扩展存储中 GM_setValue("GM_blockedParameter", blockedParameter); showPromptText("保存数据"); // 触发一次主函数,以立刻生效 FuckYouBilibiliRecommendationSystem(); }; // 关闭按钮 const closeButton = () => { // 获取需要删除的元素 let elementToRemove = document.getElementById("blockedMenuUi"); // 确保元素存在再进行删除操作 if (elementToRemove) { // 先获取父元素 let parentElement = elementToRemove.parentNode; // 在父元素删除指定的元素 parentElement.removeChild(elementToRemove); } }; // 作者主页 const authorButton = () => { setTimeout(() => { window.open("https://space.bilibili.com/351422438", "_blank"); }, 1000); showPromptText("欢迎关注!"); }; // 赞助作者 const supportButton = () => { if (!tempInputValue.QrCode_Switch) { setTimeout(() => { window.open("https://afdian.com/a/tjxgame", "_blank"); }, 1000); tempInputValue.QrCode_Switch = true; } else { tempInputValue.QrCode_Switch = false; } showPromptText("感谢老板!"); }; // 打开菜单时,先加载一次数据 refreshButton(); return { menuUiSettings, tempInputValue, addArrayButton, delArrayButton, refreshButton, saveButton, closeButton, supportButton, authorButton, }; }, }).mount("#blockedMenuUi"); } // 在油猴扩展中添加脚本菜单选项 GM_registerMenuCommand("屏蔽参数面板", blockedMenuUi); // -----------------------逻辑处理部分-------------------------- // 视频的详细信息对象,以videoBv为键, 用于同窗口内的缓存查询 let videoInfoDict = {}; // 上次输出的视频详细信息对象,用于控制台判断是否输出日志 let lastConsoleVideoInfoDict = {}; // videoInfoDict 的参考内容结构 // videoInfoDict = { // BV12i4y1e73B: { // videoLink: "https://www.bilibili.com/video/BV12i4y1e73B/", // videoTitle: "B站按 标签 标题 时长 UP主来屏蔽视频 油猴插件【tjxgame】", // videoUpName: "tjxgame", // videoUpUid: 351422438, // videoPartitions: "软件应用", // videoTags: [ // "科技2023年终总结", // "视频", // "教程", // "tjxwork", // "软件分享", // "插件", // "标签", // "屏蔽", // "油猴", // "tjxgame", // "2023热门年度盘点", // ], // topComment : "大更新,新视频!\nhttps://www.bilibili.com/video/BV1WJ4m1u79n/\n\nv1.0.0 菜单UI使用Vue3重构,现在不用担心缩放问题挡住UI了,界面更加现代化;\n改进了判断逻辑,现在可以使用白名单来避免误杀关注的UP了;\n新增功能:视频分区屏蔽、播放量屏蔽、点赞率屏蔽、竖屏视频屏蔽、UP主名称正则屏蔽、隐藏非视频元素、白名单避免屏蔽指定UP。" // whiteListTargets: true, // videoDuration: 259, // videoView: 9067, // videoLike: 507, // videoLikesRate: "5.59", // videoResolution: { // width: 3840, // height: 2160, // }, // videoChargingExclusive : false // filteredComments: false, // blockedTarget: true, // triggeredBlockedRules: [ // "屏蔽短时长视频: 259秒", // "屏蔽低播放量: 9067次", // "屏蔽低点赞率: 5.59%", // "屏蔽标题: tjxgame", // "屏蔽UP: tjxgame", // "屏蔽分区: 软件应用", // "屏蔽标签: 标签", // "屏蔽双重标签: 油猴,插件", // ], // lastVideoInfoApiRequestTime: "2024-06-21T09:17:10.389Z", // lastVideoTagApiRequestTime: "2024-06-21T09:17:10.389Z", // lastVideoCommentsApiRequestTime: "2024-06-21T09:17:10.389Z", // }, // }; // 日志输出,根据 consoleOutputLog_Switch 标志来决定是否输出日志 function consoleLogOutput(...args) { // 启用控制台日志输出 if (blockedParameter.consoleOutputLog_Switch) { // 获取当前时间的时分秒毫秒部分 let now = new Date(); let hours = now.getHours().toString().padStart(2, "0"); let minutes = now.getMinutes().toString().padStart(2, "0"); let seconds = now.getSeconds().toString().padStart(2, "0"); let milliseconds = now.getMilliseconds().toString().padStart(3, "0"); // 将时间信息添加到日志消息中 let logTime = `${hours}:${minutes}:${seconds}.${milliseconds}`; // 合并时间信息和 args 成为一个数组 let logArray = [logTime, ...args]; console.log(...logArray); } } // 简单对比对象是否不同 function objectDifferent(obj1, obj2) { if (Object.keys(obj1).length !== Object.keys(obj2).length) { return true; } for (const key in obj1) { if (obj1[key] !== obj2[key]) { return true; } } return false; } // 获取视频元素 function getVideoElements() { // // 获取所有有可能是视频元素的标签 (BewlyBewly插件的首页特殊处理) // let bewlyBewly = document.getElementById("bewly"); // if (bewlyBewly) { // // BewlyBewly插件使用shadowDOM,要在shadowDOM下面找元素 // let shadowRoot = bewlyBewly.shadowRoot; // videoElements = shadowRoot.querySelectorAll("div.video-card.group"); // // 过滤掉没有包含a标签的元素 // videoElements = Array.from(videoElements).filter((element) => element.querySelector("a")); // // 返回处理后的结果 // return videoElements; // } // BewlyBewly 更新后失效…… // 获取所有有可能是视频元素的标签 let videoElements = document.querySelectorAll( // div.bili-video-card 首页(https://www.bilibili.com/)、分区首页(https://www.bilibili.com/v/*)、搜索页面(https://search.bilibili.com/*) // div.video-page-card-small 播放页右侧推荐(https://www.bilibili.com/video/BV****) // li.bili-rank-list-video__item 分区首页-子分区右侧热门(https://www.bilibili.com/v/*) // div.video-card 综合热门(https://www.bilibili.com/v/popular/all) 、每周必看(https://www.bilibili.com/v/popular/weekly) 、入站必刷(https://www.bilibili.com/v/popular/history) // li.rank-item 排行榜(https://www.bilibili.com/v/popular/rank/all) // div.video-card-reco 旧版首页推送(https://www.bilibili.com/) // div.video-card-common 旧版首页分区(https://www.bilibili.com/) // div.rank-wrap 旧版首页分区右侧排行(https://www.bilibili.com/) "div.bili-video-card, div.video-page-card-small, li.bili-rank-list-video__item, div.video-card, li.rank-item, div.video-card-reco, div.video-card-common, div.rank-wrap" ); // 过滤掉没有包含a标签的元素 videoElements = Array.from(videoElements).filter((element) => element.querySelector("a")); // 判断是否存在旧版首页的顶部推荐条,为空的情况下再进行剔除广告元素,因为旧版首页的顶部推荐条,和新版的广告元素的类值一样…… if (document.querySelector("div.recommend-container__2-line") == null) { // 过滤掉 CSS类刚好为 'bili-video-card is-rcmd' 的元素,因为是广告。 videoElements = Array.from(videoElements).filter( (element) => element.classList.value !== "bili-video-card is-rcmd" ); } // 返回处理后的结果 return videoElements; } // 判断是否为已经屏蔽处理过的视频元素(延迟处理中) function isAlreadyBlockedChildElement(videoElement) { // // 确认是否为已经修改 元素已隐藏 跳过 // if (videoElement.style.display == "none") { // // consoleLogOutput(operationInfo, "元素已隐藏 跳过剩下主函数步骤"); // return true; // } // 确认是否为已经修改 元素已透明 延迟处理中 跳过 if (videoElement.style.filter == "blur(5px)") { // consoleLogOutput(operationInfo, "元素已透明 延迟处理中 跳过剩下主函数步骤"); return true; } // // 获取子元素,以确认是否为已经修改 // if (videoElement.firstElementChild.className == "blockedOverlay") { // // consoleLogOutput(videoElement, "获取子元素,确认是已屏蔽处理过,跳过剩下主函数步骤"); // return true; // } } // 标记为屏蔽目标,并记录命中的规则 function markAsBlockedTarget(videoBv, blockedType, blockedItem) { // 将该 Bv号 标记为屏蔽目标 videoInfoDict[videoBv].blockedTarget = true; // 确保 videoInfoDict[videoBv].triggeredBlockedRules 已定义为数组 if (!videoInfoDict[videoBv].triggeredBlockedRules) { videoInfoDict[videoBv].triggeredBlockedRules = []; } let blockedRulesItem; // 屏蔽叠加层的提示只显示类型而不显示命中项 if (blockedParameter.blockedOverlayOnlyDisplaysType_Switch) { blockedRulesItem = blockedType; } else { blockedRulesItem = blockedType + ": " + blockedItem; } // 检查是否已经这条记录 if (!videoInfoDict[videoBv].triggeredBlockedRules.includes(blockedRulesItem)) { // 将触发屏蔽的原因添加到 videoInfoDict[videoBv].triggeredBlockedRules videoInfoDict[videoBv].triggeredBlockedRules.push(blockedRulesItem); } } // 网页获取视频元素的Bv号和标题 function getBvAndTitle(videoElement) { // 从视频元素中获取所有a标签链接 const videoLinkElements = videoElement.querySelectorAll("a"); // Bv号 let videoBv; // Av号转Bv号,用于兼容 bv2av (https://greasyfork.org/zh-CN/scripts/398535),代码来源:https://socialsisteryi.github.io/bilibili-API-collect/docs/misc/bvid_desc.html#bv-av%E7%AE%97%E6%B3%95 function av2bv(aid) { const XOR_CODE = 23442827791579n; const MASK_CODE = 2251799813685247n; const MAX_AID = 1n << 51n; const BASE = 58n; const data = "FcwAPNKTMug3GV5Lj7EJnHpWsx4tb8haYeviqBz6rkCy12mUSDQX9RdoZf"; const bytes = ["B", "V", "1", "0", "0", "0", "0", "0", "0", "0", "0", "0"]; let bvIndex = bytes.length - 1; let tmp = (MAX_AID | BigInt(aid)) ^ XOR_CODE; while (tmp > 0) { bytes[bvIndex] = data[Number(tmp % BigInt(BASE))]; tmp = tmp / BASE; bvIndex -= 1; } [bytes[3], bytes[9]] = [bytes[9], bytes[3]]; [bytes[4], bytes[7]] = [bytes[7], bytes[4]]; return bytes.join(""); } // 循环处理所有a标签链接 for (let videoLinkElement of videoLinkElements) { // 已经有Bv号不需要继续了,跳过 if (videoBv) { continue; } // 处理排行榜的多链接特殊情况,符合就跳过 if (videoLinkElement.className == "other-link") { continue; } // 获取的链接,如果是Av链接的格式 let videoAvTemp = videoLinkElement.href.match(/\/(av)(\d+)/); if (videoAvTemp) { // 从链接中获取Av号 转为 Bv号 videoBv = av2bv(videoAvTemp[2]); } // 获取的链接,如果是Bv链接的格式 let videoBvTemp = videoLinkElement.href.match(/\/(BV\w+)/); if (videoBvTemp) { // 从链接中获取到 视频Bv号 videoBv = videoBvTemp[1]; } // 没拿Bv号不需要继续了,跳过 if (!videoBv) { continue; } // 确保 videoInfoDict[videoBv] 已定义 if (!videoInfoDict[videoBv]) { videoInfoDict[videoBv] = {}; } // 视频链接 videoInfoDict[videoBv].videoLink = videoLinkElement.href; } // 没有拿到Bv号,提前结束 if (!videoBv) { consoleLogOutput(videoElement, "getBvAndTitle() 没有拿到Bv号 提前结束 跳过剩下主函数步骤"); return false; } // 视频标题 , 从视频元素中获取第一个带 title 属性且不为 span 的标签 videoInfoDict[videoBv].videoTitle = videoElement.querySelector("[title]:not(span)").title; return videoBv; } // 处理匹配的屏蔽标题 function handleBlockedTitle(videoBv) { // 判断是否拿到视频标题 if (!videoInfoDict[videoBv].videoTitle) { return; } // 记录触发的规则内容 // let blockedRulesItemText = ""; // 是否启用正则 if (blockedParameter.blockedTitle_UseRegular) { // 使用 屏蔽标题数组 与 视频标题 进行匹配 const blockedTitleHitItem = blockedParameter.blockedTitle_Array.find((blockedTitleItem) => { // 正则化屏蔽标题 const blockedTitleRegEx = new RegExp(blockedTitleItem); // 判断 正则化的屏蔽标题 是否匹配 视频标题 if (blockedTitleRegEx.test(videoInfoDict[videoBv].videoTitle)) { // blockedRulesItemText = videoInfoDict[videoBv].videoTitle; return true; } }); if (blockedTitleHitItem) { // 标记为屏蔽目标并记录触发的规则 markAsBlockedTarget(videoBv, "屏蔽标题", blockedTitleHitItem); } } else { // 使用 屏蔽标题数组 与 视频标题 进行匹配 const blockedTitleHitItem = blockedParameter.blockedTitle_Array.find((blockedTitleItem) => { // 判断 屏蔽标题 是否匹配 视频标题 if (blockedTitleItem === videoInfoDict[videoBv].videoTitle) { // blockedRulesItemText = videoInfoDict[videoBv].videoTitle; return true; } }); if (blockedTitleHitItem) { // 标记为屏蔽目标并记录触发的规则 markAsBlockedTarget(videoBv, "屏蔽标题", blockedTitleHitItem); } } } // 网页获取视频UP名和UpUid (已经有API获取为什么还要网页获取?因为快……) function getNameAndUid(videoElement, videoBv) { // 如果已经有 BV号 对应的 Up主名称 Up主Uid 记录,跳过 if (videoInfoDict[videoBv].videoUpName && videoInfoDict[videoBv].videoUpUid) { return; } // 从视频元素中获取所有a标签链接 const videoLinkElements = videoElement.querySelectorAll("a"); // 循环处理所有a标签链接 for (let videoLinkElement of videoLinkElements) { // 获取的链接,如果与 Uid 的链接格式匹配的话 const uidLink = videoLinkElement.href.match(/space\.bilibili\.com\/(\d+)/); if (uidLink) { // 视频UpUid videoInfoDict[videoBv].videoUpUid = uidLink[1]; // 视频Up名称 videoInfoDict[videoBv].videoUpName = videoLinkElement.querySelector("span").innerText; } } } // API获取视频信息 function getVideoApiInfo(videoBv) { // 如果已经有BV号对应的记录,跳过 if (videoInfoDict[videoBv].videoDuration) { return; } // 当 lastVideoInfoApiRequestTime 上次API获取视频信息的时间存在,并且,和当前的时间差小于3秒时,跳过 const currentTime = new Date(); //获取当前时间 if ( videoInfoDict[videoBv].lastVideoInfoApiRequestTime && currentTime - videoInfoDict[videoBv].lastVideoInfoApiRequestTime < 3000 ) { // consoleLogOutput(videoBv, "getVideoApiInfo() 距离上次 Fetch 获取视频信息还未超过3秒钟"); return; } videoInfoDict[videoBv].lastVideoInfoApiRequestTime = currentTime; // 通过API获取视频UP信息 fetch(`https://api.bilibili.com/x/web-interface/view?bvid=${videoBv}`) .then((response) => response.json()) .then((videoApiInfoJson) => { // API获取的UP主名称: videoInfoDict[videoBv].videoUpName = videoApiInfoJson.data.owner.name; // API获取的UP主Uid: videoInfoDict[videoBv].videoUpUid = videoApiInfoJson.data.owner.mid; // API获取的视频AVid: videoInfoDict[videoBv].videoAVid = videoApiInfoJson.data.aid; // API获取的视频时长 videoInfoDict[videoBv].videoDuration = videoApiInfoJson.data.duration; // API获取的视频分区 videoInfoDict[videoBv].videoPartitions = videoApiInfoJson.data.tname; // API获取的视频播放数 videoInfoDict[videoBv].videoView = videoApiInfoJson.data.stat.view; // API获取的视频点赞数 videoInfoDict[videoBv].videoLike = videoApiInfoJson.data.stat.like; // 计算视频点赞率保留2位小数 videoInfoDict[videoBv].videoLikesRate = ( (videoInfoDict[videoBv].videoLike / videoInfoDict[videoBv].videoView) * 100 ).toFixed(2); // // API获取的视频投币数 // videoInfoDict[videoBv].videoCoin = videoApiInfoJson.data.stat.coin; // // API获取的视频收藏数 // videoInfoDict[videoBv].videoFavorite = videoApiInfoJson.data.stat.favorite; // // API获取的视频分享数 // videoInfoDict[videoBv].videoShare = videoApiInfoJson.data.stat.share; // // API获取的视频评论数 // videoInfoDict[videoBv].videoReply = videoApiInfoJson.data.stat.reply; // // API获取的视频弹幕数 // videoInfoDict[videoBv].videoDanmaku = videoApiInfoJson.data.stat.danmaku; // API获取的视频是否为充电专属 videoInfoDict[videoBv].videoChargingExclusive = videoApiInfoJson.data.is_upower_exclusive; // API获取的视频分辨率 if (!videoInfoDict[videoBv].videoResolution) { videoInfoDict[videoBv].videoResolution = {}; } videoInfoDict[videoBv].videoResolution.width = videoApiInfoJson.data.dimension.width; videoInfoDict[videoBv].videoResolution.height = videoApiInfoJson.data.dimension.height; FuckYouBilibiliRecommendationSystem(); }) .catch((error) => consoleLogOutput(videoBv, "getVideoApiInfo() Fetch错误:", error)); } // 处理匹配短时长视频 function handleBlockedShortDuration(videoBv) { // 判断是否拿到视频时长 if (!videoInfoDict[videoBv].videoDuration) { return; } // 判断设置的屏蔽短时长视频值 是否大于 视频时长 if (blockedParameter.blockedShortDuration > videoInfoDict[videoBv].videoDuration) { // 标记为屏蔽目标并记录触发的规则 markAsBlockedTarget(videoBv, "屏蔽短时长视频", videoInfoDict[videoBv].videoDuration + "秒"); } } // 处理 屏蔽低播放量视频 function handleBlockedBelowVideoViews(videoBv) { // 判断是否拿到视频播放量 if (!videoInfoDict[videoBv].videoView) { return; } // 判断设置的屏蔽视频点赞率值 是否大于 视频的点赞率 if (blockedParameter.blockedBelowVideoViews > videoInfoDict[videoBv].videoView) { // 标记为屏蔽目标并记录触发的规则 markAsBlockedTarget(videoBv, "屏蔽低播放量", videoInfoDict[videoBv].videoView + "次"); } } // 处理匹配屏蔽低于指定点赞率的视频 function handleBlockedBelowLikesRate(videoBv) { // 判断是否拿到视频点赞数 if (!videoInfoDict[videoBv].videoLikesRate) { return; } // 判断设置的屏蔽视频点赞率值 是否大于 视频的点赞率 if (blockedParameter.blockedBelowLikesRate > videoInfoDict[videoBv].videoLikesRate) { // 标记为屏蔽目标并记录触发的规则 markAsBlockedTarget(videoBv, "屏蔽低点赞率", videoInfoDict[videoBv].videoLikesRate + "%"); } } // 处理匹配屏蔽竖屏视频 function handleBlockedPortraitVideo(videoBv) { // 判断是否拿到视频分辨率 if (!videoInfoDict[videoBv].videoResolution.width) { return; } // 横向分辨率小于纵向分辨率就是竖屏 if (videoInfoDict[videoBv].videoResolution.width < videoInfoDict[videoBv].videoResolution.height) { // 标记为屏蔽目标并记录触发的规则 markAsBlockedTarget( videoBv, "屏蔽竖屏视频", `${videoInfoDict[videoBv].videoResolution.width} x ${videoInfoDict[videoBv].videoResolution.height}` ); } } // 处理匹配 屏蔽充电专属视频 function handleBlockedChargingExclusive(videoBv) { // 判断设置的屏蔽充电专属视频是否有启用标记 if (videoInfoDict[videoBv].videoChargingExclusive) { // 标记为屏蔽目标并记录触发的规则 markAsBlockedTarget(videoBv, "屏蔽充电专属的视频", videoInfoDict[videoBv].videoUpName); } } // 处理匹配的屏蔽Up主名称或Up主Uid function handleBlockedNameOrUid(videoBv) { // 判断是否拿到Up主名称或Up主Uid if (!videoInfoDict[videoBv].videoUpUid) { return; } // 记录触发的规则内容 let blockedRulesItemText = ""; // 是否启用正则 if (blockedParameter.blockedNameOrUid_UseRegular) { // 使用 屏蔽Up名称和Uid数组 与 视频Up主Uid 和 视频Up主名称 进行匹配 const blockedNameOrUidHitItem = blockedParameter.blockedNameOrUid_Array.find((blockedNameOrUidItem) => { // 正则化屏蔽Up主名称、视频Up主Uid const blockedNameOrUidRegEx = new RegExp(blockedNameOrUidItem); // 只有UP名称有正则的意义,Uid依然是直接对比 if (blockedNameOrUidRegEx.test(videoInfoDict[videoBv].videoUpName)) { blockedRulesItemText = videoInfoDict[videoBv].videoUpName; return true; } if (blockedNameOrUidItem == videoInfoDict[videoBv].videoUpUid) { blockedRulesItemText = videoInfoDict[videoBv].videoUpUid; return true; } }); if (blockedNameOrUidHitItem) { // 标记为屏蔽目标并记录触发的规则 markAsBlockedTarget(videoBv, "屏蔽UP", blockedRulesItemText); } } else { // 使用 屏蔽Up名称和Uid数组 与 视频Up主Uid 和 视频Up主名称 进行匹配 const blockedNameOrUidHitItem = blockedParameter.blockedNameOrUid_Array.find((blockedNameOrUidItem) => { if (blockedNameOrUidItem == videoInfoDict[videoBv].videoUpName) { blockedRulesItemText = videoInfoDict[videoBv].videoUpName; return true; } if (blockedNameOrUidItem == videoInfoDict[videoBv].videoUpUid) { blockedRulesItemText = videoInfoDict[videoBv].videoUpUid; return true; } }); if (blockedNameOrUidHitItem) { // 标记为屏蔽目标并记录触发的规则 markAsBlockedTarget(videoBv, "屏蔽UP", blockedRulesItemText); } } } // 处理匹配的屏蔽视频分区 function handleBlockedVideoPartitions(videoBv) { // 判断是否拿到视频分区 if (!videoInfoDict[videoBv].videoPartitions) { return; } // 记录触发的规则内容 let blockedRulesItemText = ""; // 是否启用正则 if (blockedParameter.blockedVideoPartitions_UseRegular) { // 使用 屏蔽视频分区数组 与 视频分区 进行匹配 const blockedVideoPartitionsHitItem = blockedParameter.blockedVideoPartitions_Array.find( (blockedVideoPartitionsItem) => { // 正则化屏蔽视频标签 const blockedVideoPartitionsRegEx = new RegExp(blockedVideoPartitionsItem); if (blockedVideoPartitionsRegEx.test(videoInfoDict[videoBv].videoPartitions)) { blockedRulesItemText = videoInfoDict[videoBv].videoPartitions; return true; } } ); if (blockedVideoPartitionsHitItem) { // 标记为屏蔽目标并记录触发的规则 markAsBlockedTarget(videoBv, "屏蔽分区", blockedRulesItemText); } } else { // 使用 屏蔽视频分区数组 与 视频分区 进行匹配 const blockedVideoPartitionsHitItem = blockedParameter.blockedVideoPartitions_Array.find( (blockedVideoPartitionsItem) => { if (blockedVideoPartitionsItem == videoInfoDict[videoBv].videoPartitions) { blockedRulesItemText = videoInfoDict[videoBv].videoPartitions; return true; } } ); if (blockedVideoPartitionsHitItem) { // 标记为屏蔽目标并记录触发的规则 markAsBlockedTarget(videoBv, "屏蔽分区", blockedRulesItemText); } } } // API获取视频标签 function getVideoApiTags(videoBv) { // 如果已经有BV号对应的记录,跳过 if (videoInfoDict[videoBv].videoTags) { return; } // 当 lastVideoTagApiRequestTime 上次API获取视频标签的时间存在,并且,和当前的时间差小于3秒时,跳过 const currentTime = new Date(); //获取当前时间 if ( videoInfoDict[videoBv].lastVideoTagApiRequestTime && currentTime - videoInfoDict[videoBv].lastVideoTagApiRequestTime < 3000 ) { // consoleLogOutput(videoBv, "getVideoApiTags() 距离上次 Fetch 获取视频信息还未超过3秒钟"); return; } videoInfoDict[videoBv].lastVideoTagApiRequestTime = currentTime; // 获取视频标签 fetch(`https://api.bilibili.com/x/web-interface/view/detail/tag?bvid=${videoBv}`) .then((response) => response.json()) .then((videoApiTagsJson) => { // API获取标签对象,提取标签名字数组 videoInfoDict[videoBv].videoTags = videoApiTagsJson.data.map((tagsArray) => tagsArray.tag_name); FuckYouBilibiliRecommendationSystem(); }) .catch((error) => consoleLogOutput(videoBv, "getVideoApiTags() Fetch错误:", error)); } // 处理匹配的屏蔽标签 function handleBlockedTag(videoBv) { // 判断是否拿到视频标签 if (!videoInfoDict[videoBv].videoTags) { return; } // 记录触发的规则内容 let blockedRulesItemText = ""; // 是否启用正则 if (blockedParameter.blockedTag_UseRegular) { // 使用 屏蔽标签数组 与 视频标题数组 进行匹配 const blockedTagHitItem = blockedParameter.blockedTag_Array.find((blockedTagItem) => { // 正则化屏蔽视频标签 const blockedTagRegEx = new RegExp(blockedTagItem); // 使用 屏蔽标签正则 和 视频标题数组 进行匹配 const videoTagHitItem = videoInfoDict[videoBv].videoTags.find((videoTagItem) => blockedTagRegEx.test(videoTagItem) ); if (videoTagHitItem) { blockedRulesItemText = videoTagHitItem; return true; } }); if (blockedTagHitItem) { // 标记为屏蔽目标并记录触发的规则 markAsBlockedTarget(videoBv, "屏蔽标签", blockedRulesItemText); } } else { // 使用 屏蔽标签数组 与 视频标题数组 进行匹配 const blockedTagHitItem = blockedParameter.blockedTag_Array.find((blockedTagItem) => { // 使用 屏蔽标签 和 视频标题数组 进行匹配 const videoTagHitItem = videoInfoDict[videoBv].videoTags.find( (videoTagItem) => blockedTagItem == videoTagItem ); if (videoTagHitItem) { blockedRulesItemText = videoTagHitItem; return true; } }); if (blockedTagHitItem) { // 标记为屏蔽目标并记录触发的规则 markAsBlockedTarget(videoBv, "屏蔽标签", blockedRulesItemText); } } } // 处理匹配屏蔽双重屏蔽标签 function handleDoubleBlockedTag(videoBv) { // 判断是否拿到视频标签 if (!videoInfoDict[videoBv].videoTags) { return; } // 记录触发的规则内容 let blockedRulesItemText = ""; // 是否启用正则 if (blockedParameter.doubleBlockedTag_UseRegular) { // 使用 双重屏蔽标签数组 与 视频标签 进行匹配 const doubleBlockedTagHitItem = blockedParameter.doubleBlockedTag_Array.find((doubleBlockedTag) => { // 以 "|" 分割成数组,同时都能匹配上才是符合 const doubleBlockedTagSplitArray = doubleBlockedTag.split("|"); const doubleBlockedTagRegEx0 = new RegExp(doubleBlockedTagSplitArray[0]); const doubleBlockedTagRegEx1 = new RegExp(doubleBlockedTagSplitArray[1]); const videoTagHitItem0 = videoInfoDict[videoBv].videoTags.find((videoTagItem) => doubleBlockedTagRegEx0.test(videoTagItem) ); const videoTagHitItem1 = videoInfoDict[videoBv].videoTags.find((videoTagItem) => doubleBlockedTagRegEx1.test(videoTagItem) ); if (videoTagHitItem0 && videoTagHitItem1) { blockedRulesItemText = `${videoTagHitItem0},${videoTagHitItem1}`; return true; } }); if (doubleBlockedTagHitItem) { // 标记为屏蔽目标并记录触发的规则 markAsBlockedTarget(videoBv, "屏蔽双重标签", blockedRulesItemText); } } else { // 使用 双重屏蔽标签数组 与 视频标签 进行匹配 const doubleBlockedTagHitItem = blockedParameter.doubleBlockedTag_Array.find((doubleBlockedTag) => { // 以 "|" 分割成数组,同时都能匹配上才是符合 const doubleBlockedTagSplitArray = doubleBlockedTag.split("|"); const videoTagHitItem0 = videoInfoDict[videoBv].videoTags.find( (videoTagItem) => doubleBlockedTagSplitArray[0] == videoTagItem ); const videoTagHitItem1 = videoInfoDict[videoBv].videoTags.find( (videoTagItem) => doubleBlockedTagSplitArray[1] == videoTagItem ); if (videoTagHitItem0 && videoTagHitItem1) { blockedRulesItemText = `${videoTagHitItem0},${videoTagHitItem1}`; return true; } }); if (doubleBlockedTagHitItem) { // 标记为屏蔽目标并记录触发的规则 markAsBlockedTarget(videoBv, "屏蔽双重标签", blockedRulesItemText); } } } // API获取视频评论区 let apiRequestDelayTime = 0; function getVideoApiComments(videoBv) { // 如果已经有BV号对应的记录,跳过 if (videoInfoDict[videoBv].filteredComments === false || videoInfoDict[videoBv].filteredComments === true) { return; } // 当 lastVideoCommentsApiRequestTime 上次API获取视频评论区的时间存在,并且,和当前的时间差小于3秒时,跳过 const currentTime = new Date(); //获取当前时间 if ( videoInfoDict[videoBv].lastVideoCommentsApiRequestTime && currentTime - videoInfoDict[videoBv].lastVideoCommentsApiRequestTime < 3000 ) { // consoleLogOutput(videoBv, "getVideoApiComments() 距离上次 Fetch 获取视频信息还未超过3秒钟"); return; } // 获取评论区的API貌似对频繁请求的容忍度很低,只能错开来请求,apiRequestDelayTime 延迟。 // 所以设置了每次调用 getVideoApiComments() 都会增加延迟,例如:每次加 50ms 再请求下一个请求。 // lastVideoCommentsApiRequestTime(上次API获取视频评论区的时间) 本质是为了限制每个BV号3秒只能请求一次, // 但是加了延迟之后,到后面 apiRequestDelayTime 延迟本身就会超过3秒了。 // 还是会出现多次请求的问题,可能影响不大,但是还是把延迟值加进了 lastVideoCommentsApiRequestTime 里面。 // 这也相当于把 lastVideoCommentsApiRequestTime 修正为了正确请求时间。 let apiRequestDelayTimeData = new Date(apiRequestDelayTime); videoInfoDict[videoBv].lastVideoCommentsApiRequestTime = new Date( currentTime.getTime() + apiRequestDelayTimeData.getTime() ); // apiRequestDelayTime 的最大值限制问题 // 如果不做限制的话,这个值可能会无限增大,导致最后加载的视频元素的请求也永远等不到生效时间。 // 以 videoInfoDict 对象的长度来做最大值限制貌似会比较合理一点。但是这个对象也可能会无限增大从而导致后面的请求等太久。 // 如果把 videoInfoDict[videoBv].filteredComments 筛选为 null 后的统计数值x延迟时间,做为最大延迟时间比较好? // lastVideoCommentsApiRequestTime 也保证了每个Bv号的对应请求3秒只出现一次,这样就不用担心重复请求的问题。 // 但是本质上这一堆处理只是为了:防止频繁请求 https://api.bilibili.com/x/v2/reply 出现拒绝,同时为了效率的问题,每个Bv号只应该请求一次。 // 统计 videoInfoDict 中,视频Bv下面的 filteredComments 不存在的数量。 function filteredCommentsCount() { let nullCount = 0; for (const video in videoInfoDict) { if (videoInfoDict[video].hasOwnProperty("filteredComments") == false) { nullCount++; } } return nullCount; } // 最大的延迟时间上限 let apiRequestDelayTimeMax = filteredCommentsCount() * 100; // consoleLogOutput("最大的延迟时间上限", apiRequestDelayTimeMax); // 每次调用增加的延迟 > 最大的延迟时间上限后 重置为0 if (apiRequestDelayTime > apiRequestDelayTimeMax) { apiRequestDelayTime = 0; } setTimeout(() => { // 设置请求的 URL 和参数 const url = "https://api.bilibili.com/x/v2/reply"; const params = { type: 1, // 评论区类型代码 oid: videoBv, // 目标评论区 id sort: 0, // 排序方式,默认为0,0:按时间,1:按点赞数,2:按回复数 ps: 1, // 每页项数,默认为20,定义域:1-20 pn: 1, // 页码,默认为1 nohot: 0, // 是否不显示热评,默认为0,1:不显示,0:显示 }; // 将参数转换为 URL 搜索字符串 const searchParams = new URLSearchParams(params).toString(); // 获取视频评论区 fetch(`${url}?${searchParams}`) .then((response) => response.json()) .then((VideoApiCommentsJson) => { // API获取精选评论标记 videoInfoDict[videoBv].filteredComments = VideoApiCommentsJson.data?.control?.web_selection; // API获取置顶评论内容 videoInfoDict[videoBv].topComment = VideoApiCommentsJson.data.upper.top?.content?.message; FuckYouBilibiliRecommendationSystem(); }) .catch((error) => consoleLogOutput(videoBv, "getVideoApiComments() Fetch错误:", error)); }, apiRequestDelayTime); // 每次调用增加的延迟 // consoleLogOutput("本次调用增加延迟", apiRequestDelayTime); apiRequestDelayTime = apiRequestDelayTime + 100; } // 处理匹配 屏蔽精选评论的视频 function handleBlockedFilteredCommentsVideo(videoBv) { // 判断设置的屏蔽精选评论的视频是否有启用标记 if (videoInfoDict[videoBv].filteredComments) { // 标记为屏蔽目标并记录触发的规则 markAsBlockedTarget(videoBv, "屏蔽精选评论的视频", videoInfoDict[videoBv].videoUpName); } } // 处理匹配 屏蔽置顶评论内容 function handleBlockedTopComment(videoBv) { // 判断是否拿到视频置顶评论 if (!videoInfoDict[videoBv].topComment) { return; } // 记录触发的规则内容 // let blockedRulesItemText = ""; // 是否启用正则 if (blockedParameter.blockedTopComment_UseRegular) { // 使用 屏蔽置顶评论数组 与 置顶评论 进行匹配 const blockedTopCommentHitItem = blockedParameter.blockedTopComment_Array.find((blockedTopComment) => { // 正则化屏蔽置顶评论 const blockedTitleRegEx = new RegExp(blockedTopComment); // 判断 正则化的屏蔽置顶评论 是否匹配 置顶评论 if (blockedTitleRegEx.test(videoInfoDict[videoBv].topComment)) { return true; } }); if (blockedTopCommentHitItem) { // 标记为屏蔽目标并记录触发的规则 markAsBlockedTarget(videoBv, "屏蔽置顶评论", blockedTopCommentHitItem); } } else { // 使用 屏蔽置顶评论数组 与 置顶评论 进行匹配 const blockedTopCommentHitItem = blockedParameter.blockedTopComment_Array.find((blockedTopComment) => { // 判断 屏蔽置顶评论 是否匹配 置顶评论 if (blockedTopComment === videoInfoDict[videoBv].topComment) { return true; } }); if (blockedTopCommentHitItem) { // 标记为屏蔽目标并记录触发的规则 markAsBlockedTarget(videoBv, "屏蔽置顶评论", blockedTopCommentHitItem); } } } // 处理匹配的白名单Up主和Uid function handleWhitelistNameOrUid(videoBv) { // 判断是否拿到Up主名称或Up主Uid if (!videoInfoDict[videoBv].videoUpUid) { return; } // 使用 白名单Up主和Uid数组 与 视频Up主Uid 和 视频Up主名称 进行匹配 const videoNameOrUid = blockedParameter.whitelistNameOrUid_Array.find((whitelistNameOrUidItem) => { if (whitelistNameOrUidItem == videoInfoDict[videoBv].videoUpName) { return true; } if (whitelistNameOrUidItem == videoInfoDict[videoBv].videoUpUid) { return true; } }); if (videoNameOrUid) { // 标记为白名单目标 videoInfoDict[videoBv].whiteListTargets = true; } } // 隐藏非视频元素 function hideNonVideoElements() { // 判断当前页面URL是否以 https://www.bilibili.com/ 开头,即首页 if (window.location.href.startsWith("https://www.bilibili.com/")) { // 隐藏首页的番剧、国创、直播等左上角有标的元素,以及左上角没标的直播 const adElements_1 = document.querySelectorAll("div.floor-single-card, div.bili-live-card"); adElements_1.forEach(function (element) { element.style.display = "none"; }); } // 判断当前页面URL是否以 https://search.bilibili.com/all 开头,即搜索页——综合 if (window.location.href.startsWith("https://search.bilibili.com/all")) { // 隐藏 搜索页——综合 下的 直播卡片 const adElements_2 = document.querySelectorAll("div.bili-video-card:has(div.bili-video-card__info--living)"); adElements_2.forEach(function (element) { element.parentNode.style.display = "none"; element.style.display = "none"; }); } // 隐藏首页广告,那些没有“enable-no-interest” CSS类的视频卡片元素 const adElements_3 = document.querySelectorAll("div.bili-video-card.is-rcmd:not(.enable-no-interest)"); adElements_3.forEach(function (element) { // 检查其父元素是否是 .feed-card if (element.closest("div.feed-card") !== null) { // 如果是,选择其父元素并应用样式 element.closest("div.feed-card").style.display = "none"; } else { // 如果不是,直接在视频元素上应用样式 element.style.display = "none"; } }); // 隐藏视频播放页右侧广告、视频相关的游戏推荐、视频相关的特殊推荐、大家围观的直播 const adElements_4 = document.querySelectorAll( "div#slide_ad, a.ad-report, div.video-page-game-card-small, div.video-page-special-card-small, div.pop-live-small-mode" ); adElements_4.forEach(function (element) { element.style.display = "none"; }); } // 屏蔽或者取消屏蔽 function blockedOrUnblocked(videoElement, videoBv, setTimeoutStatu = false) { // 是白名单目标,是屏蔽目标,没有隐藏、没有叠加层:跳过 if ( videoInfoDict[videoBv].whiteListTargets && videoInfoDict[videoBv].blockedTarget && videoElement.style.display != "none" && videoElement.firstElementChild.className != "blockedOverlay" ) { return; } // 是白名单目标,是屏蔽目标, 有隐藏或有叠加层:去除隐藏或叠加层 if ( videoInfoDict[videoBv].whiteListTargets && videoInfoDict[videoBv].blockedTarget && (videoElement.style.display == "none" || videoElement.firstElementChild.className == "blockedOverlay") ) { // 去除叠加层 removeHiddenOrOverlay(videoElement, videoBv, setTimeoutStatu); return; } // 不是白名单目标,是屏蔽目标, 有隐藏或有叠加层:跳过 if ( videoInfoDict[videoBv].whiteListTargets != true && videoInfoDict[videoBv].blockedTarget && (videoElement.style.display == "none" || videoElement.firstElementChild.className == "blockedOverlay") ) { return; } // 不是白名单目标,是屏蔽目标, 没有隐藏、没有叠加层:隐藏或添加叠加层 if ( videoInfoDict[videoBv].whiteListTargets != true && videoInfoDict[videoBv].blockedTarget && videoElement.style.display != "none" && videoElement.firstElementChild.className != "blockedOverlay" ) { // 隐藏或添加叠加层 addHiddenOrOverlay(videoElement, videoBv, setTimeoutStatu); return; } // 隐藏或添加叠加层 function addHiddenOrOverlay(videoElement, videoBv, setTimeoutStatu) { // 是否为隐藏视频模式? if (blockedParameter.hideVideoMode_Switch == true) { // 隐藏视频 // 判断当前页面URL是否以 https://search.bilibili.com/ 开头,即搜索页面,修改父元素 if (window.location.href.startsWith("https://search.bilibili.com/")) { videoElement.parentNode.style.display = "none"; // 为什么改了父元素,还要改元素本身?为了方便上面的判断。 videoElement.style.display = "none"; } // 如果是父元素是feed-card,修改父元素 else if (videoElement.closest("div.feed-card") !== null) { videoElement.closest("div.feed-card").style.display = "none"; videoElement.style.display = "none"; } else { videoElement.style.display = "none"; } } else { // 添加叠加层 // Bug记录: // 位置: 视频播放页面 (即 https://www.bilibili.com/video/BVxxxxxx 页面下) // 行为: 添加屏蔽叠加层 这个操作 只因为 屏蔽标签 的方式来触发时 (如果还触发了 屏蔽标题 屏蔽短时长 这一类,是不会出现这个Bug的。) // 症状: 渲染异常,右侧视频推荐列表的封面图片不可见;评论区丢失;页面头部的搜索框丢失 (div.center-search__bar 丢失); // 处理: 延迟添加 overlay 可解决,先暂时把元素变成透明/模糊的,等3秒,页面完全加载完了,再创建创建屏蔽叠加层,再把元素改回正常。 // 猜测: 我一开始以为是使用 fetch 获取API造成的,因为只有 屏蔽标签 这个操作必须通过 fetch 获取标签信息的。 // 但是出现 屏蔽标题 屏蔽短时长 多种触发的情况下,又不会触发这个Bug了,想不懂,我也不会调试这种加载过程。 // 在 视频播放页面 "card-box" 创建屏蔽叠加层操作作延迟处理 if (videoElement.firstElementChild.className == "card-box" && setTimeoutStatu == false) { // 元素先改模糊 // videoElement.style.opacity = "0"; videoElement.style.filter = "blur(5px)"; // 延迟3秒 setTimeout(() => { // 创建屏蔽叠加层 blockedOrUnblocked(videoElement, videoBv, true); // 元素再改回正常 // videoElement.style.opacity = "1"; videoElement.style.filter = "none"; }, 3000); return; } // 获取 videoElement 的尺寸 const elementRect = videoElement.getBoundingClientRect(); // 叠加层参数(背景) let overlay = document.createElement("div"); overlay.className = "blockedOverlay"; overlay.style.position = "absolute"; overlay.style.width = elementRect.width + "px"; // 使用 videoElement 的宽度 overlay.style.height = elementRect.height + "px"; // 使用 videoElement 的高度 overlay.style.backgroundColor = "rgba(60, 60, 60, 0.85)"; overlay.style.display = "flex"; overlay.style.justifyContent = "center"; overlay.style.alignItems = "center"; overlay.style.zIndex = "10"; overlay.style.backdropFilter = "blur(6px)"; overlay.style.borderRadius = "6px"; // 叠加层文本参数(背景) let overlayText = document.createElement("div"); if (videoElement.firstElementChild.className == "card-box") { overlayText.style.fontSize = "1.25em"; } // 使用 videoInfoDict[videoBv] 里面的存储的触发规则的第1条来做为提示文字 overlayText.innerText = videoInfoDict[videoBv].triggeredBlockedRules[0]; overlayText.style.color = "rgb(250,250,250)"; overlay.appendChild(overlayText); // 添加叠加层为最前面的子元素 videoElement.insertAdjacentElement("afterbegin", overlay); } } // 去除隐藏或叠加层 function removeHiddenOrOverlay(videoElement) { // 是否为隐藏视频模式? if (blockedParameter.hideVideoMode_Switch == true) { // 取消隐藏 // 判断当前页面URL是否以 https://search.bilibili.com/ 开头,即搜索页面 if (window.location.href.startsWith("https://search.bilibili.com/")) { videoElement.parentNode.style.display = ""; videoElement.style.display = ""; } // 如果是父元素是feed-card else if (videoElement.closest("div.feed-card") !== null) { videoElement.closest("div.feed-card").style.display = ""; videoElement.style.display = ""; } else { videoElement.style.display = ""; } } else { // 删除叠加层 if (videoElement.firstElementChild.className == "blockedOverlay") { videoElement.removeChild(videoElement.firstElementChild); } } } } // 同步屏蔽叠加层与父元素的尺寸 function syncBlockedOverlayAndParentNodeRect() { // 获取所有的屏蔽叠加层 const blockedOverlays = document.querySelectorAll("div.blockedOverlay"); blockedOverlays.forEach(function (element) { // 获取父元素的尺寸 const parentNodeElementRect = element.parentNode.getBoundingClientRect(); // 修改屏蔽叠加层的大小 element.style.width = parentNodeElementRect.width + "px"; // 使用 父元素的尺寸 的宽度 element.style.height = parentNodeElementRect.height + "px"; // 使用 父元素的尺寸 的高度 }); } // -----------------主流程函数---------------------- // 屏蔽Bilibili上的符合屏蔽条件的视频 function FuckYouBilibiliRecommendationSystem() { // 是否启用 隐藏非视频元素 if (blockedParameter.hideNonVideoElements_Switch) { // 隐藏非视频元素 hideNonVideoElements(); } // 判断是否和上次的输出的字典不一样 if (objectDifferent(lastConsoleVideoInfoDict, videoInfoDict)) { // 输出整个视频信息字典 consoleLogOutput(Object.keys(videoInfoDict).length, "个视频信息: ", videoInfoDict); // 将本次输出的视频信息字典保存起来作参考 lastConsoleVideoInfoDict = Object.assign({}, videoInfoDict); } // 获取所有包含B站视频相关标签的视频元素 const videoElements = getVideoElements(); // 遍历每个视频元素 for (let videoElement of videoElements) { // 判断是否为已经屏蔽处理过的子元素 if (isAlreadyBlockedChildElement(videoElement)) { // 如果是已经屏蔽处理过的子元素,跳过后续操作 continue; } // 网页获取视频元素的Bv号和标题 let videoBv = getBvAndTitle(videoElement); // 如果没有拿到Bv号,跳过后续操作 if (!videoBv) { continue; } // 是否启用 屏蔽标题 if (blockedParameter.blockedTitle_Switch && blockedParameter.blockedTitle_Array.length > 0) { // 判断处理匹配的屏蔽标题 handleBlockedTitle(videoBv); } // 网页获取视频Up名和UpUid getNameAndUid(videoElement, videoBv); // 通过API获取视频信息 getVideoApiInfo(videoBv); // 是否启用 屏蔽Up主名称或Up主Uid if (blockedParameter.blockedNameOrUid_Switch && blockedParameter.blockedNameOrUid_Array.length > 0) { // 判断处理匹配的屏蔽Up主名称或Up主Uid handleBlockedNameOrUid(videoBv); } // 是否启用 屏蔽视频分区 if ( blockedParameter.blockedVideoPartitions_Switch && blockedParameter.blockedVideoPartitions_Array.length > 0 ) { // 判断处理匹配 屏蔽视频分区 handleBlockedVideoPartitions(videoBv); } // 是否启用 屏蔽短时长视频 if (blockedParameter.blockedShortDuration_Switch && blockedParameter.blockedShortDuration > 0) { // 判断处理匹配的短时长视频 handleBlockedShortDuration(videoBv); } // 是否启用 屏蔽低播放量视频 if (blockedParameter.blockedBelowVideoViews_Switch && blockedParameter.blockedBelowVideoViews > 0) { // 判断处理匹配的低播放量视频 handleBlockedBelowVideoViews(videoBv); } // 是否启用 屏蔽低于指定点赞率的视频 if (blockedParameter.blockedBelowLikesRate_Switch && blockedParameter.blockedBelowLikesRate > 0) { // 判断处理 屏蔽低于指定点赞率的视频 handleBlockedBelowLikesRate(videoBv); } // 是否启用 屏蔽竖屏视频 if (blockedParameter.blockedPortraitVideo_Switch) { // 判断处理 屏蔽竖屏视频 handleBlockedPortraitVideo(videoBv); } // 是否启用 屏蔽充电专属视频 if (blockedParameter.blockedChargingExclusive_Switch) { // 判断处理 蔽充电专属视频 handleBlockedChargingExclusive(videoBv); } // 通过API获取视频标签 getVideoApiTags(videoBv); // 是否启用 屏蔽标签 if (blockedParameter.blockedTag_Switch && blockedParameter.blockedTag_Array.length > 0) { // 判断处理 屏蔽标签 handleBlockedTag(videoBv); } // 是否启用 屏蔽双重屏蔽标签 if (blockedParameter.doubleBlockedTag_Switch && blockedParameter.doubleBlockedTag_Array.length > 0) { // 判断处理 屏蔽双重屏蔽标签 handleDoubleBlockedTag(videoBv); } // API获取视频评论区 getVideoApiComments(videoBv); // 是否启用 屏蔽精选评论的视频 if (blockedParameter.blockedFilteredCommentsVideo_Switch) { // 判断处理 屏蔽精选评论的视频 handleBlockedFilteredCommentsVideo(videoBv); } // 是否启用 屏蔽置顶评论 if (blockedParameter.blockedTopComment_Switch && blockedParameter.blockedTopComment_Array.length > 0) { // 判断处理 屏蔽精选评论的视频 handleBlockedTopComment(videoBv); } // 是否启用 白名单Up主和Uid if (blockedParameter.whitelistNameOrUid_Switch && blockedParameter.whitelistNameOrUid_Array.length > 0) { // 判断处理 白名单Up主和Uid handleWhitelistNameOrUid(videoBv); } // 屏蔽或者取消屏蔽 blockedOrUnblocked(videoElement, videoBv); // 同步屏蔽叠加层与父元素的尺寸 syncBlockedOverlayAndParentNodeRect(); } } // 页面加载完成后运行脚本 window.addEventListener("load", FuckYouBilibiliRecommendationSystem); // 窗口尺寸变化时运行脚本 window.addEventListener("resize", FuckYouBilibiliRecommendationSystem); // 定义 MutationObserver 的回调函数 function mutationCallback() { // 在这里运行你的脚本 FuckYouBilibiliRecommendationSystem(); } // 创建一个 MutationObserver 实例,观察 body 元素的子节点变化 let observer = new MutationObserver(mutationCallback); let targetNode = document.body; // 配置观察器的选项 let config = { childList: true, subtree: true }; // 启动观察器并传入回调函数和配置选项 observer.observe(targetNode, config);